<?php
/**
 * a DataMapper pattern implementation
 * stores objects in database using query templates
 * This file is part of Webappkit, a packages system for PHP web apps
 * @link http://webappkit.net
 * @package webappkit
 * @author J.Ducastel <nospam0@ducastel.name>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */
class DbMapperTool extends WakTool implements iWakStorage {

    /**
     * @var array default configuration to be overrided
     * [storage][search] => $this->search search pattern
     * [storage][path]
     * [storage][class] => $this->stored_class stored objects class
     * [storage][id_member] object's unique key member
     * [criteria] => $this->criteria
     */
    protected $cfg=array(
        'storage'=>array(
            'class'=>'' // stored objects class
            ,'id_member'=>'id') // which objects member is the unique key/id ?
        ,'has_one'=>array() // member name => class
        ,'has_many'=>array() // member name => items class
        //,'criteria'=>array() // default values for search criteria
        , 'queries'=>array() // queries if stored internally
    );

    public static function toolFactory($kit_id,$cfg=array()) {
        return new DbMapperTool($kit_id,$cfg);
    }

    /**
     * there should be a has query returning a COUNT(*)
     */
    public function has($id) {
        // getting query
        $sql=$this->_buildQuery('has',array('id'=>$id));
        // getting result
        $result=$this->_executeSql($sql);
        $line=mysql_fetch_row($result);
        return intval($line[0]);

    }

    /**
     * gets an object instance from database
     * @param string $id
     * @param bool $eager if true, retrieve object members too
     * @return object
     */
    public function get($id, $eager=true) {
        return $this->_instanceFromQuery('get',array('id'=>$id));
    }

    /**
     * lists instances from database
     * @return array(string id)
     * @todo
     */
    public function getList() {
        
    }

    /**
     * save an object instance into database
     * tries update (if id is set)
     * , then replace, then insert
     * If primary key member is not set, will set the key from mysql_last_insert_id
     * @param object $instance
     * @param string $query if null, will
     * @param string $key_member primary key member name, default is 'id'
     * @return bool
     */
    public function save(&$instance) {
        $id_member=$this->getIdMember();
        if (isset($instance->$id_member)) {
            // update
            if (!$this->_saveInstance($instance,'update')
            and !$this->_saveInstance($instance,'replace'))
                return false;
        // trying insertion
        } else if (!$this->_saveInstance($instance,'insert',true)
        /*and !$this->_saveInstance($instance,'replace',)*/) {
                // throw new Exception("insertion failed: ".$this->SqlError());
                return false;
        }
        // saving has_many associations
        if (!isset($this->cfg['has_many']))
            return true;
        foreach ($this->cfg['has_many'] as $name => $class)
            if (!$this->_saveHasMany($instance,$name,$class))
                return false;
        return true;
    }

    /**
     * delete an object into database.
     * set object's key generated value
     * @param object $instance
     * @return bool
     */
    public function delete($id) {
        $sql=$this->_buildQuery('delete',array('id'=>$id));
        if (!$this->_executeSql($sql))
            return false;
        return true;
    }

    /**
     * search for instances
     * @param array $params
     * @return array
     */
    public function search($params) {

    }

    public function getClass() {
        return $this->cfg['storage']['class'];
    }

    public function getIdMember() {
        return $this->cfg['storage']['member_id'];
    }

    public function SqlError() {
        return Mysql_error().' '.$this->last_query;
    }

    /**
     * builds an instance from source data
     * @param array $data
     * @return object
     */
    public function factory($data,$eager=true) {
        $class=$this->getClass();
        // listing class vars
        $keys=array_keys(get_class_vars($class)); //print_r($keys);
        // building instance
        $instance=new $class();
        // looping class keys
        foreach ($keys as $key) {
            // dumping data result if present
            if (isset($data[$key]))
                $instance->$key=$data[$key];
            // fetching associations
            else if ($eager) {
                $this->_memberFactory($instance,$key);
                //$member=$this->_instanceFromQuery()
            }
        }
        return $instance;
    }

/*------------------------------------------------------------------------------
  protected / private elements
------------------------------------------------------------------------------*/


    /**
     * afterbuild initialization
     * searches for complementary DB tools within same kit
     * /
    protected function _init() {
        $kit=&Webappkit::getKit($this->getKitId()); // echo '<pre>'; print_r($kit);
        // getting dblink
        if ($tools=$kit->getTools('DbLinkTool')) {
            //$this->dblink=&$tools[0];
            // print_r($tools);
            $this->dblink=current($tools);
        }
    }/**/

    /**
     * search for query, fetch result and build instance from result
     * Searches query templates from $cfg[queries]
     * , then within a QueryTplStorage
     * @param string $query query id
     * @param array|object $params
     * @return object
     */
    protected function & _instanceFromQuery($query,$params=array()) {
        $sql=$this->_buildQuery($query,$params);
        $result=$this->_executeSql($sql);
        if (!$values=mysql_fetch_assoc($result)) {
            throw new DbNotFound("DbMapperTool: no result for ".$this->last_query);
        }
        $instance=&$this->factory($values);
        return $instance;
    }

    /**
     * saves an object into database
     * @param object $instance
     * @param string $query defaults to update
     */
    protected function _saveInstance(&$instance,$query='update',$set_id=false) {
        // build query
        if (!$sql=$this->_buildQuery($query,$instance)) {
            return false;
        }// echo $sql;
        if (!$result=$this->_executeSql($sql)
        or mysql_affected_rows()<1) {
            return false;
        }
        // settign id member if required
        $id_member=$this->getIdMember();
        if ($set_id and !isset($instance->$id_member)) {
            $id=mysql_insert_id();
            $instance->$id_member=$id;
        }
        return true;
    }

    /**
     * builds and add object member from dedicated queries
     * @param object $instance
     * @param string $name
     * @return bool
     */
    protected function _memberFactory(&$instance,$name) {
        // search for "has one" members
        if (isset($this->cfg['has_one'][$name])) {
            $instance->$name=$this->_getHasOne($instance,$name
                ,$this->cfg['has_one'][$name]);
        } else if (isset($this->cfg['has_many'][$name])) {
            $instance->$name=$this->_getHasMany($instance,$name
                ,$this->cfg['has_many'][$name]);
        }
        return false;
    }

    /**
     * fetch, build and adds "has one" members
     * @param object $instance
     * @param string $member member name
     * @param string $class member class
     * @return object
     */
    protected function _getHasOne(&$instance,$member,$class) {
        // search for query
        $query=$member.'.get';
        if (!$sql=$this->_buildQuery($query,$instance))
            throw new Exception("query $query not found");
        if (!$result=$this->_executeSql($sql)
        or !$data=mysql_fetch_assoc($result)) {
            throw new DbNotFound("no result to catch member $member");
        }
        $member=new $class();
        $submembers=array_keys(get_class_vars($class));
        foreach ($submembers as $key) {
            if (isset($data[$key]))
                $member->$key=$data[$key];
        }
        return $member;
    }/**/

    /**
     * fetch, build and adds "has many" members
     * @param object $instance
     * @param string $name member name
     * @param string $class member class
     */
    protected function _getHasMany(&$instance,$name,$class) {
        // search for query
        $query=$name.'.get';
        if (!$sql=$this->_buildQuery($query,$instance))
            throw new Exception("query $query not found");
        if (!$result=$this->_executeSql($sql)) {
            throw new DbNotFound("no result to catch member $name");
        }
        $collection=array();
        while ($data=mysql_fetch_assoc($result)) {
            // print_r($data);
            $member=new $class();
            $submembers=array_keys(get_class_vars($class));
            foreach ($submembers as $key) {
                if (isset($data[$key]))
                    $member->$key=$data[$key];
            }
            array_push($collection,$member);
            //$instance->$name=$member;
        }
        return $collection;
    }

    /**
     * save has-many associations
     * @param object $instance
     * @param string $name
     * @param string $class
     */
    protected function _saveHasMany(&$instance,$name,$class) {
        // get current saved collection
        $saved=$this->_getHasMany($instance,$name,$class);
        // add new elements
        foreach ($instance->$name as $item) {
            if (!in_array($item,$saved)) {
                $this->_add($instance,$name,$item);
            }
        }
        // remove old elements
        foreach ($saved as $item) {
            if (!in_array($item,$instance->$name)) {
                $this->_remove($instance,$name,$item);
            }
        }
        return true;
    }

    /**
     * adds an item to master collection
     * @param object $master
     * @param string $name which master member the slave belongs to
     * @param object $slave
     * @return bool
     */
    protected function _add(&$master,$name,$slave) {
        $data=array('master'=>$master,'slave'=>$slave);
        $sql=$this->_buildQuery($name.'.add',$data); // echo $sql;
        return $this->_executeSql($sql);
    }

    /**
     * remove an item to master collection
     * @param object $master
     * @param string $name which master member the slave belongs to
     * @param object $slave
     * @return bool
     */
    protected function _remove(&$master,$name,$slave) {
        $data=array('master'=>$master,'slave'=>$slave);
        $sql=$this->_buildQuery($name.'.remove',$data); // echo $sql;
        return $this->_executeSql($sql);
    }

    /**
     * Search for query template.
     * May search within a QueryTplStorage within same kit
     * @todo sanitizing params
     * @param string $query query id
     * @param array $data query parameters
     * @return string SQL query
     */
    private function _buildQuery($query,$data=array()) {
        // id should be lowercase
        $query=strtolower($query);
        // searching for query template
        if (isset($this->cfg['queries'][$query])) {
            // found within config data
            $tpl=new DbQueryTpl($this->cfg['queries'][$query]);
        } else if (false) {
            // found within QueryTplStorage
        } else {
            // not found
            // throw new Exception("DbMapperTool: no query $query",500);
            return false;
        }
        // do sanitizing here ? done by Querytpl
        // injecting parameters
        return $tpl->render($data);
    }

    /**
     * execute SQL query .
     * May use DbLinkTool within self kit if available
     * @param string $sql
     * @return resource
     */
    private function _executeSql($sql) {
        $this->last_query=$sql;
        if (!$r=mysql_query($sql)) {
            //throw new DbSqlError('DbMapperTool: '.mysql_error().': '.$sql,500);
            return false;
        }
        return $r;
    }

    /**
     * @var string last SQL query executed
     */
    protected $last_query;
}
